﻿/*
 * Copyright 2008 Pimm Hogeling
 * 
 * This file is part of Bomberleague. Contributions to this file were made
 * by (feel free to add your name and/or e-mail to this list if you believe
 * you should be mentioned here):
 * Pimm Hogeling <pimmhogeling@gmail.com>
 * 
 * Bomberleague is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 * 
 * Bomberleague is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Bomberleague. If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.bomberleague.input {
	import com.googlecode.bomberleague.events.KeyEvent;
	import flash.events.EventDispatcher;
	import flash.utils.ByteArray;
	public class InputController {
		private static var _eventDispatcher:EventDispatcher = new EventDispatcher();
		/**
		 * This ByteArray object contains 2048 bits that represent 2048 inputs. Each "1" bit determines that this input is attached
		 *  to a key, while each "0" bit means that this input is not attached to a key. The KeyboardDevice Class only takes up the
		 *  0-255 range, so the other 1792 inputs can be used for gamepads, joysticks, mouse inputs etc.
		 */
		private static var inputBits:ByteArray;
		/**
		 * This ByteArray object contains 256 shorts that represent 256 keys. Each short contains the current state of this key (up
		 *  or down) in its first bit and the input that is attached to that key in the eleven bits before that.
		 */
		private static var keys:ByteArray;
		/**
		 * If this uint object is non-zero then the key set mode is in progress and this object contains the data for the key set
		 *  mode, if this object is zero then the key set mode is not in progress.
		 */
		private static var keySetMode:uint;
		public static function initialize(fingerprint:String = null):void {
			if (fingerprint) {
				keyFingerprint = fingerprint;
			} else {
				inputBits = new ByteArray();
				inputBits.length = 0x100; // Set the length to 0x100 (256) bytes, or 0x800 (2048) bits. Enough for 2048 inputs.
				keys = new ByteArray();
				keys.length = 0x200; // Set the length to 0x100 (256) shorts or 0x200 (512) bytes. Enough for 16 players (1 "global player" and 15 real players) with 16 keys each.
			}
		}
		/**
		 * Attaches an input to a key.
		 * 
		 * @param key               <uint> The key to attach the input to.
		 * @param input             <uint> The input to attach to the key.
		 */
		public static function attachToKey(key:uint, input:uint):void {
			const k:uint = key & 0xFF; // key & 0xFF to make sure it doesn't exceed 256.
			const i:uint = input & 0x7FF; // input & 0x7FF to make sure it doesn't exceed 2048.
			keySetMode = 0;
			if (!setInputBit(i, true)) { // Set the input that will be attached to this key to true, if it was already true that means this input was already attached to another key.
				const oldKey:uint = findAttachedKey(i); // Find which key that was.
				keys.position = oldKey << 1;
				keys.writeShort(0); // Write a zero (no input attached) to the old key.
				_eventDispatcher.dispatchEvent(new KeyEvent(KeyEvent.KEY_SET, oldKey, true, 0));
			}
			keys.position = k << 1;
			setInputBit(keys.readUnsignedShort() >> 1, false); // Set the input that used to be attached to this key to false.
			keys.position -= 2;
			keys.writeShort(i << 1);
			_eventDispatcher.dispatchEvent(new KeyEvent(KeyEvent.KEY_SET, k, true, input));
		}
		/**
		 * This EventDispatcher object dispatches the KeyEvent.KEY and KeyEvent.KEY_SET events.
		 */
		public static function get eventDispatcher():EventDispatcher {
			return _eventDispatcher;
		}
		private static function findAttachedKey(input:uint):uint {
			keys.position = 0;
			while (true) { // Check every short in keys object. Could use "while (keys.bytesAvailable)", but this is faster.
				if (keys.readShort() >> 1 === input) { // If the code in the short equals the given code, we have a match!
					return (keys.position - 2) >> 1;
				}
			}
			return 0;
		}
		/**
		 * Returns the input that is attached to this key.
		 * 
		 * @param key               <uint> The input attached to this key is returned.
		 * @return                  <uint> The input that is attached to the supplied key. There are 2048 possible inputs so this
		 *                                  value will always be in the 000-7FF range.
		 */
		public static function getAttachedInput(key:uint):uint {
			keys.position = (key & 0xFF) << 1; // key & 0xFF to make sure it doesn't exceed 256.
			return keys.readUnsignedShort() >> 1;
		}
		private static function getInputBit(input:uint):Boolean {
			const pos:uint = 1 << (input & 0x7) // The last three bits of the input determine the place of the bit in the byte.
			inputBits.position = input >> 3; // The eight bits before that determine the position of the byte in the inputBits ByteArray object.
			return Boolean(inputBits.readUnsignedByte() & pos);
		}
		/**
		 * A big hex String object that contains the entire key configuration. Keep in mind that setting this property will
		 *  overwrite the current key configuration. Also keep in mind that both getting and setting this property requires some
		 *  calculations and will take up some CPU.
		 */
		public static function get keyFingerprint():String {
			keys.position = 0;
			var f:ByteArray = new ByteArray();
			while (keys.bytesAvailable) {
				f.writeShort(keys.readUnsignedShort() & 0x1FE); // Write the contents of the keys ByteArray object to the f ByteArray object, except for the last bit.
			}
			f.compress(); // Compress the f ByteArray object.
			var r:String = "";
			f.position = 2; // Skip the "78DA" algorithm code.
			var a:String;
			while (f.bytesAvailable) {
				a = f.readUnsignedByte().toString(16); // Read bytes from f ByteArray object and convert them to hex.
				if (a.length & 1) {
					a = "0" + a; // Add a leading zero (0) if the new hex code is only one char.
				}
				r += a;
			}
			return r;
		}
		public static function set keyFingerprint(value:String):void {
			initialize();
			var i:uint;
			keys.length = 0;
			keys.writeShort(0x78DA); // Add the leading "78DA" algorithm code.
			while (value.length > i + 1) {
				keys.writeByte(uint("0x" + value.substr(i, 2))); // Convert a part of the string object to a byte and write that to the keys ByteArray object.
				i += 2;
			}
			keys.uncompress();
			while (keys.bytesAvailable) {
				setInputBit(keys.readUnsignedShort() >> 1, true);
			}
		}
		private static function setInputBit(input:uint, value:Boolean):Boolean {
			const pos:uint = 1 << (input & 0x7) // The last three bits of the input determine the place of the bit in the byte.
			inputBits.position = input >> 3; // The eight bits before that determine the position of the byte in the inputBits ByteArray object.
			const currentByte:uint = inputBits.readUnsignedByte();
			if (value === !(currentByte & pos)) { // If the new value is the opposite of the old one.
				inputBits.position -= 1;
				inputBits.writeByte(currentByte ^ pos);
				return true;
			}
			return false;
		}
		/**
		 * This method sets the state of an input. No need to worry about which key this input is attached to, this method will
		 *  figure it out. This method will most likely be called by InputDevices.
		 * 
		 * @param input             <uint> The state of this input will be set.
		 * @param value             <Boolean> The state to set for this input.
		 */
		public static function setInputState(input:uint, value:Boolean):void {
			if (!keySetMode) { // If we are not in key set mode.
				if (getInputBit(input)) { // If this input is set to true, in other words if this input is attached to a key.
					const key:uint = findAttachedKey(input); // Find the attached key.
					if (setKeyState(key, value)) { // Set the state of the key to value, if the new state is the opposite of the old one.
						_eventDispatcher.dispatchEvent(new KeyEvent(KeyEvent.KEY, key, value, input)); // Dispatch an event.
					}
				}
			} else if (value) { // We don't want to be setting/attaching a key because the user puts an input in up state.
				attachToKey(keySetMode, input);
			}
		}
		private static function setKeyState(key:uint, value:Boolean):Boolean {
			keys.position = key << 1;
			const currentShort:uint = keys.readUnsignedShort();
			if (value === !(currentShort & 0x1)) { // If the new value is the opposite of the old one.
				keys.position -= 2;
				keys.writeShort(currentShort ^ 0x1);
				return true;
			}
			return false;
		}
		public static function startKeySetMode(key:uint):void {
			keySetMode = key & 0xFF; // key & 0xFF to make sure it doesn't exceed 256.
			keySetMode ^= 0x100; // Sets bit 9 to true so even if key is zero, when keySetMode is typecasted to a Boolean it will become true.
		}
	}
}